﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using S7Lib.Base;

namespace S7Lib
{
    public class S7Client
    {
        private Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

        private int pduSize = 0;

        /// <summary>
        /// PLC 建立连接 
        ///     TCP => COTP => Set Communication
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="rack"></param>
        /// <param name="slot"></param>
        /// <param name="timeout"></param>
        /// <exception cref="Exception"></exception>
        public void Connect(string ip,int port,byte rack,byte slot,int timeout = 5000)
        {
            socket.ReceiveTimeout = timeout;
            socket.Connect(ip, port);

            // COTP
            byte[] cotp = new byte[]
            {
                // TPKT
                0x03,0x00,
                0x00,0x16,
                // COTP
                0x11,
                0xe0,
                0x00,0x00,
                0x00,0x01,
                0x00,
                // C0
                0x00,
                0x01,
                0x0a,
                // C1
                0xc1,
                0x02,
                0x10,
                0x00,
                // C2
                0xc2,
                0x02,
                0x03,
                (byte)(rack * 32 + slot)
            };

            this.SendAndReceive(cotp);

            // Setup Communication
            byte[] buff = new byte[]
            {
                 // TPKT
                0x03,0x00,
                0x00,0x19,
                // COTP
                0x02,
                0xf0,
                0x80,
                // S7 header
                0x32,
                0x01,
                0x00,0x00,
                0x00,0x00,

                // param length
                0x00,0x08,
                // data length
                0x00,0x00,

                // S7 Parameter
                0xf0,
                0x00,
                0x00,0x03,
                0x00,0x03,

                // pdu size
                0x03,0xe0
            };
            byte[] resp_bytes = this.SendAndReceive(buff);
            // 17 18
            if(resp_bytes[14] != 0x00)
            {
                throw new Exception("连接异常：Setup Communication失败");
            }
            // 25 26
            pduSize = BitConverter.ToUInt16(new byte[] { resp_bytes[22], resp_bytes[21] });
        }

        public void Read(params S7Parameter[] variable)
        {
            byte[] param_bytes = GetS7Parameters(variable);
            byte[] body_bytes = new byte[]
            {
                // COTP
                0x02,
                0xf0,
                0x80,
                // S7 Header
                0x32,
                0x01,
                0x00,0x00,
                0x00,0x00,

                (byte)((param_bytes.Length + 2) / 256),
                (byte)((param_bytes.Length + 2) % 256),

                0x00,0x00,
                0x04,
                (byte)variable.Count(),
            };

            byte[] req_bytes = new byte[]
            {
                // TPTK
                0x03,0x00,
                (byte)((body_bytes.Length + 4) / 256),
                (byte)((body_bytes.Length + 4) % 256),
            };

            byte[] resp_bytes = this.SendAndReceive(req_bytes);
            ushort resp_status = (ushort)((resp_bytes[13] << 8) + resp_bytes[14]);
            if (Status.HeaderErrors.ContainsKey(resp_status))
            {
                throw new Exception(Status.HeaderErrors[resp_status]);
            }
            int idx = 17;
            for (int i = 0; i < variable.Length; i++)
            {
                if (resp_bytes[idx] != 0xff)
                    throw new Exception(Status.DataReturnCode[resp_bytes[idx]]);
                idx++;
                ushort len = (ushort)(resp_bytes[idx + 1] * 256 + resp_bytes[idx + 2]);
                if (resp_bytes[idx] == 0x04)
                    len /= 8;
                variable[i].DataBytes = resp_bytes.ToList().GetRange(idx + 3, len).ToArray();

                // 包含了Item1的的有字节数  再往后偏移一个字节
                // 数据字节数 + 状态字节和字节数字节 + 往后偏移一个 + FillBytes字节
                // (data_bytes.Length % 2)当数据字节为奇数个时，添加一个FillBytes字节
                idx += 2 + len + len % 2 + 1;
            }
        }

        public byte[] Read(string variable,int count = 1)
        {
            S7Parameter param = GetVariable(variable,count);
            this.Read(param);
            return param.DataBytes;
        }

        public void Write(params S7Parameter[] variable)
        {
            byte[] ps = GetS7Parameters(variable);
            byte[] ds = GetS7Datas(variable);

            List<byte> datas = new List<byte>
            {
                // COTP
                0x02,
                0xf0,
                0x80,
                // S7 Header
                0x32,
                0x01,
                0x00,0x00,
                0x00,0x00,
                (byte)((ps.Length + 2) / 256 % 256),
                (byte)((ps.Length + 2) % 256),
                (byte)(ds.Length / 256),
                (byte)(ds.Length % 256),
                // S7 Parameter
                0x04,(byte)variable.Length
            };
            datas.AddRange(ps);
            datas.AddRange(ds);

            List<byte> req_bytes = new List<byte>
            {
                0x03,0x00,
                (byte)((datas.Count + 4)/ 256),
                (byte)((datas.Count + 4) % 256),
            };
            req_bytes.AddRange(datas);

            byte[] resp_bytes = this.SendAndReceive(req_bytes.ToArray());
            ushort resp_status = (ushort)(resp_bytes[13] * 256 + resp_bytes[14]);
            if (Status.HeaderErrors.ContainsKey(resp_status))
            {
                throw new Exception(Status.HeaderErrors[resp_status]);
            }
            int offset = 17;
            for (int i = 0; i < variable.Length; i++)
            {
                if (resp_bytes[offset] != 0xff)
                    throw new Exception(Status.DataReturnCode[resp_bytes[offset]]);
                offset++;
            }
        }

        public void Write(string variable, byte[] bytes)
        {
            S7Parameter param = GetVariable(variable, 1);
            param.DataBytes = bytes;
            if (param.PVarType == ParameterVarType.BYTE)
            {
                param.Count = bytes.Length;
            }
            else if (param.PVarType == ParameterVarType.WORD)
            {
                param.Count = bytes.Length / 2;
            }
            else if(param.PVarType == ParameterVarType.DWORD)
            {
                param.Count = bytes.Length / 4;
            }

            param.DVarType = DataVarType.BYTE;
            if (param.PVarType == ParameterVarType.BIT)
                param.DVarType = DataVarType.BIT;
            this.Write(param);
        }

        public byte[] SendAndReceive(byte[] data)
        {
            socket.Send(data);
            byte[] head = new byte[4];
            socket.Receive(head,0,head.Length,SocketFlags.None);

            int len = head[2] * 256 + head[3] - 4;
            byte[] buff = new byte[len];
            socket.Receive(buff, 0, buff.Length, SocketFlags.None);
            return buff;
        }

        // 只对一个地址进行处理  
        // DB1.DBW100
        // Read("DB1.DBW100",2)
        /// I      I0.0         IB0        IW0        ID0
        /// Q      Q0.0         QB.....
        /// M
        /// V      V10.5        VB10       VW10       VD10
        /// DB     DB1.DBX0.0   DB1.DBB0   DB1.DBW0   DB1.DBD0
        public S7Parameter GetVariable(string variable,int count)
        {
            S7Parameter param = new S7Parameter();
            param.Count = count;

            string str_head = variable.Substring(0, 2).ToUpper();
            if ("DB".Equals(str_head))
            {
                param.Area = Area.DB;
                string[] str_arr = variable.Substring(0, 2).Split('.');
                param.DBNumber = ushort.Parse(str_arr[0]);

                if(str_arr.Length == 3)
                {
                    param.PVarType = ParameterVarType.BIT;
                    if(!int.TryParse(str_arr[1].Substring(3),out int byteAddr))
                        throw new Exception("地址错误，无法解析");
                    param.ByteAddress = byteAddr;
                    if (!byte.TryParse(str_arr[2], out byte bitAddr))
                        throw new Exception("地址错误，无法解析");
                    param.BitAddress = bitAddr;
                    param.Count = 1;
                }
                else
                {
                    string st = str_arr[1].Substring(2, 1);
                    param.PVarType = ParamVarDic[st];
                    if (!int.TryParse(str_arr[1].Substring(3), out int byteAddr))
                        throw new Exception("地址错误，无法解析");
                    param.ByteAddress = byteAddr;
                }
            }
            else if ("IQMV".Equals(variable.Substring(0,1).ToUpper()))
            {
                // 根据第一个字符进行判断
                param.Area = AreaDic[variable[0]];
                // 如果是针对V区  这里设置为1   其他需要为0
                param.DBNumber = (ushort)(variable[0] == 'V' ? 1 : 0);

                string[] str_arr = variable.Split('.');

                // IQMV  contains  '.' 
                if(str_arr.Length == 2)
                {
                    if (!int.TryParse(str_arr[0].Substring(1), out int byteAddr))
                        throw new Exception("地址错误，无法解析");
                    param.ByteAddress = byteAddr;
                    if (!byte.TryParse(str_arr[1], out byte bitAddr))
                        throw new Exception("地址错误，无法解析");
                    param.BitAddress= bitAddr;
                    param.Count = 1;
                }
                else
                {// VB10 VW100 VD120
                    string st = str_arr[0].Substring(1, 1);
                    param.PVarType= ParamVarDic[st];
                    if (!int.TryParse(str_arr[0].Substring(2),out int byteAddr))
                        throw new Exception("地址错误，无法解析");
                    param.ByteAddress = byteAddr;
                }
            }
            else
            {
                throw new Exception("地址格式不支持，无法解析");
            }
            return param;
        }

        public void DisConnect()
        {
            if (socket == null || !socket.Connected) return;

            socket.Disconnect(true);
            socket.Close();
            socket.Dispose();
        }

        private Dictionary<string, ParameterVarType> ParamVarDic = new Dictionary<string, ParameterVarType>
        {
            { "X",ParameterVarType.BIT},
            { "B",ParameterVarType.BYTE},
            { "W",ParameterVarType.WORD},
            { "D",ParameterVarType.DWORD},
        };
        private Dictionary<char, Area> AreaDic = new Dictionary<char, Area>
        {
            {'I',Area.I },
            {'Q',Area.Q },
            {'M',Area.M },
            {'V',Area.DB }
        };

        private byte[] GetS7Parameters(S7Parameter[] variable)
        {
            List<byte> list = new List<byte>();
            int sum = 0;
            foreach (var item in variable)
            {
                int num = 1;
                if (item.PVarType == ParameterVarType.WORD)
                    num = 2;
                else if (item.PVarType == ParameterVarType.DWORD)
                    num = 4;
                num = item.Count * num;
                num += num % 2;
                sum += num;

                byte[] bytes = new byte[]
                {
                    0x12,
                    0x0a,
                    0x10,
                    (byte)item.PVarType,
                    (byte)(item.Count / 256),
                    (byte)(item.Count % 256),

                    (byte)(item.DBNumber / 256),
                    (byte)(item.DBNumber % 256),

                    (byte)item.Area,
                    (byte)(((item.ByteAddress << 3)+ item.BitAddress) / 256 / 256 % 256),
                    (byte)(((item.ByteAddress << 3)+ item.BitAddress) / 256 % 256),
                    (byte)(((item.ByteAddress << 3)+ item.BitAddress) % 256),
                };

                list.AddRange(bytes);
            }
            if (sum > pduSize - 50)
                throw new Exception("请确认请求数据量");
            return list.ToArray();
        }

        private byte[] GetS7Datas(S7Parameter[] variable)
        {
            List<byte> list = new List<byte>();
            for (int i = 0; i < variable.Length; i++)
            {
                int bit = 1;
                if (variable[i].DVarType == DataVarType.BYTE)
                    bit = 8;
                List<byte> bytes = new List<byte>
                {
                    0x00,
                    (byte)variable[i].DVarType,
                    (byte)(variable[i].DataBytes.Length * bit / 256),
                    (byte)(variable[i].DataBytes.Length * bit % 256)
                };
                bytes.AddRange(variable[i].DataBytes);

                if (variable[i].DataBytes.Length % 2 == 1 && i < variable.Length - 1)
                {
                    bytes.Add(0x00);
                }
                list.AddRange(bytes);
            }

            return list.ToArray();
        }

        // 功能有限制   200Smart/300/400有用
        public void Run()
        {
            byte[] buff = new byte[]
            {
                // TPKT
                0x03,
                0x00,
                0x00,0x25, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x01,
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x14,  // 10#20
                // Data Length
                0x00,0x00,

                // S7-Parameter
                0x28,// 控制PLC启动
                0x00,0x00,0x00,0x00,0x00,
                0x00,0xfd,
                0x00,0x00,
                
                0x09,   // 字符长度
                // "P_PROGRAM"
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
            };

            byte[] resp_bytes = this.SendAndReceive(buff);
            ushort resp_status = (ushort)((resp_bytes[13] << 8) +  resp_bytes[14]);
            if (Status.HeaderErrors.ContainsKey(resp_status))
            {
                throw new Exception(Status.HeaderErrors[resp_status]);
            }
        }

        // 功能有限制   200Smart/300/400有用
        public void Stop()
        {
            byte[] buff = new byte[]
            {
                // TPKT
                0x03,
                0x00,
                0x00,0x21, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x01,
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x10,  // 10#20
                // Data Length
                0x00,0x00,

                // S7-Parameter
                0x29,// 控制PLC停止
                0x00,0x00,0x00,0x00,0x00,

                0x09,   // 字符长度
                // P_PROGRAM
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
            };

            byte[] resp_bytes = this.SendAndReceive(buff);
            ushort resp_status = (ushort)((resp_bytes[13] << 8) + resp_bytes[14]);
            if (Status.HeaderErrors.ContainsKey(resp_status))
            {
                throw new Exception(Status.HeaderErrors[resp_status]);
            }
        }

        public void SetTime(DateTime time)
        {
            byte year = Convert.FromHexString((time.Year - 2000).ToString("00"))[0];
            byte month = Convert.FromHexString(time.Month.ToString("00"))[0];
            byte day = Convert.FromHexString(time.Day.ToString("00"))[0];
            byte hour = Convert.FromHexString(time.Hour.ToString("00"))[0];
            byte minute = Convert.FromHexString(time.Minute.ToString("00"))[0];
            byte second = Convert.FromHexString(time.Second.ToString("00"))[0];

            byte[] buff = new byte[]
            {
                // TPKT
                0x03,0x00,
                0x00,0x27, // 39

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x07,// UserData
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x08,
                // Data Length
                0x00,0x0E, // 14


                // S7-Paramter
                0x00,0x01,0x12,
                0x04,
                0x11,
                0x47,

                0x02,// Set Clock
                0x00,

                // S7-Data
                0xff,
                0x09,
                0x00,0x0a,// 后续有10个长度的字节
                0x00,
                0x19,
                // Year:25
                year,
                // Month:10
                month,
                // Day:01
                day,
                // Hour:20
                hour,
                // Minute:05
                minute,
                // Second:00
                second,
                // 毫秒:0  和星期
                0x00,0x00
                // 0000 0000   0000 0  100
            };

            byte[] resp_bytes = this.SendAndReceive(buff);
            ushort header_error_code = (ushort)((resp_bytes[23] << 8) + resp_bytes[24]);
            if (header_error_code != 0)
            {
                throw new Exception(Status.HeaderErrors[header_error_code]);
            }
        }

        public DateTime GetTime()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03,
                0x00,
                0x00,0x1d, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x07,//UserData
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x8,  // 10#20
                // Data Length
                0x00,0x04,

                // S7-Parameter
                0x00,0x01,0x12,
                0x04,// Parameter部分后续还有4个字节的内容
                0x11,
                // 0100 0111
                0x47,
                0x01,// Read Clock
                0x00,

                // S7-Data
                0x0a,
                0x00,
                0x00,0x00
            };

            byte[] resp = this.SendAndReceive(bytes);

            ushort header_error_code = (ushort)((resp[23] << 8) + resp[24]);
            if (header_error_code != 0)
            {
                throw new Exception(Status.HeaderErrors[header_error_code]);
            }
            else if (resp[25] != 0xff)
            {
                throw new Exception(Status.DataReturnCode[resp[25]]);
            }

            return DateTime.Now;
        }

        public List<T> GetData<T>(byte[] datas)
        {
            List<T> list = new List<T>();
            if (typeof(T) == typeof(bool))
            {
                foreach (var item in datas)
                {
                    dynamic val = item == 0x01;
                    list.Add(val);
                }
            }
            // 这个处理逻辑不支持200Smart的字符串处理
            else if (typeof(T) == typeof(string))
            {
                //byte[0]   254  字符串的有效空间
                //byte[1]   有效字符的字节数
                List<byte> strList = datas.ToList().GetRange(2, datas[1]);
                dynamic d = Encoding.ASCII.GetString(strList.ToArray());
                list.Add(d);
            }
            else
            {
                int size = Marshal.SizeOf(typeof(T));
                Type bitConverter = typeof(BitConverter);
                System.Reflection.MethodInfo[] methods = bitConverter.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
                if (methods.Length <= 0) return list;

                System.Reflection.MethodInfo? method = methods.FirstOrDefault(m => m.ReturnType == typeof(T) && m.GetParameters().Length == 2);
                if (method == null)
                    throw new Exception("Can't Find a Convert Method for this type");
                for (int i = 0; i < datas.Length; i += size)
                {
                    List<byte> ts = datas.ToList().GetRange(i, size);
                    dynamic v = method.Invoke(bitConverter, new object[] { ts.ToArray(), 0 });
                    list.Add(v);
                }
            }
            return list;
        }

        public byte[] GetBytes<T>(params T[] datas)
        {
            List<byte> bytes = new List<byte>();
            if (typeof(T) == typeof(bool))
            {
                foreach (var item in datas)
                {
                    bytes.Add((byte)(bool.Parse(item.ToString()) ? 0x01 : 0x00));
                }
            }
            else if (typeof(T) == typeof(string))
            {
                byte[] buff = Encoding.ASCII.GetBytes(datas[0].ToString());
                bytes.AddRange(buff);
            }
            else
            {
                foreach (var data in datas)
                {
                    dynamic dt = data;
                    byte[] v_bytes = BitConverter.GetBytes(dt);
                    if(BitConverter.IsLittleEndian) 
                        Array.Reverse(v_bytes);
                    bytes.AddRange(v_bytes);
                }
            }
            return bytes.ToArray();
        }
    }
}
